﻿using JESAI.Schedule.Cron.Consts;
using JESAI.Schedule.Cron.CronExpressions.Accumulators;
using JESAI.Schedule.Cron.Exceptions;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace JESAI.Schedule.Cron.CronExpressions
{
    sealed partial class CronExpressionFieldPool
    {
        public static readonly CronExpressionFieldPool Second = new(CronFieldKind.Second, 0, 59, null);
        public static readonly CronExpressionFieldPool Minute = new(CronFieldKind.Minute, 0, 59, null);
        public static readonly CronExpressionFieldPool Hour = new(CronFieldKind.Hour, 0, 23, null);
        public static readonly CronExpressionFieldPool Day = new(CronFieldKind.Day, 1, 31, null);
        public static readonly CronExpressionFieldPool Month = new(CronFieldKind.Month, 1, 12, ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]);
        public static readonly CronExpressionFieldPool Week = new(CronFieldKind.DayOfWeek, 0, 6, ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]);

        public static readonly CronExpressionFieldPool Year = new(CronFieldKind.Year, 1, 12, null);

        static readonly CronExpressionFieldPool[] FieldByKind = [Second, Minute, Hour, Day, Month, Week, Year];


        static readonly CompareInfo Comparer = CultureInfo.InvariantCulture.CompareInfo;

        public CronFieldKind Kind { get; }

        public int MinValue { get; }
        public int MaxValue { get; }

        public int ValueCount => MaxValue - MinValue + 1;

        readonly string[]? names;

        CronExpressionFieldPool(CronFieldKind kind, int minValue, int maxValue, string[]? names)
        {
            Debug.Assert(Enum.IsDefined(typeof(CronFieldKind), kind));
            Debug.Assert(minValue >= 0);
            Debug.Assert(maxValue >= minValue);
            Debug.Assert(names == null || names.Length == (maxValue - minValue + 1));

            Kind = kind;
            MinValue = minValue;
            MaxValue = maxValue;
            this.names = names;
        }

        public static CronExpressionFieldPool FromKind(CronFieldKind kind)
        {
            if (!Enum.IsDefined(typeof(CronFieldKind), kind))
            {
                var kinds = string.Join(", ", Enum.GetNames(typeof(CronFieldKind)));
                throw new ArgumentException($"Invalid crontab field kind. Valid values are {kinds}.", nameof(kind));
            }

            return FieldByKind[(int)kind];
        }

        public void Format(ICronExpressionField field, TextWriter writer) =>
       Format(field, writer, false);

        public void Format(ICronExpressionField field, TextWriter writer, bool noNames)
        {
            if (field == null) throw new ArgumentNullException(nameof(field));
            if (writer == null) throw new ArgumentNullException(nameof(writer));

            var next = field.GetFirst();
            var count = 0;

            while (next != -1)
            {
                var first = next;
                int last;

                do
                {
                    last = next;
                    next = field.Next(last + 1);
                }
                while (next - last == 1);

                if (count == 0
                    && first == MinValue && last == MaxValue)
                {
                    writer.Write('*');
                    return;
                }

                if (count > 0)
                    writer.Write(',');

                if (first == last)
                {
                    FormatValue(first, writer, noNames);
                }
                else
                {
                    FormatValue(first, writer, noNames);
                    writer.Write('-');
                    FormatValue(last, writer, noNames);
                }

                count++;
            }
        }

        void FormatValue(int value, TextWriter writer, bool noNames)
        {
            if (noNames || this.names == null)
            {
                if (value is >= 0 and < 100)
                {
                    FastFormatNumericValue(value, writer);
                }
                else
                {
                    writer.Write(value.ToString(CultureInfo.InvariantCulture));
                }
            }
            else
            {
                var index = value - MinValue;
                writer.Write(this.names[index]);
            }
        }

        static void FastFormatNumericValue(int value, TextWriter writer)
        {
            Debug.Assert(value is >= 0 and < 100);

            if (value >= 10)
            {
                writer.Write((char)('0' + (value / 10)));
                writer.Write((char)('0' + (value % 10)));
            }
            else
            {
                writer.Write((char)('0' + value));
            }
        }

        public void Parse(string str, CronFieldAccumulator<ExceptionHandle?> acc) => _ = TryParse(str, acc, null, static ep => throw ep());

        public T TryParse<T>(string str, CronFieldAccumulator<T> acc, T success, Func<ExceptionHandle, T> errorSelector)
        {
            if (acc == null) throw new ArgumentNullException(nameof(acc));

            if (string.IsNullOrEmpty(str))
                return success;

            try
            {
                return InternalParse(str, acc, success, errorSelector);
            }
            catch (Exception e) when (e is FormatException or CronException)
            {
                return errorSelector(() => new CronException($"'{str}' is not a valid [{Kind}] crontab field expression.", e));
            }
        }

        T InternalParse<T>(string str, CronFieldAccumulator<T> acc, T success, Func<ExceptionHandle, T> errorSelector)
        {
            if (str.Length == 0)
                return errorSelector(() => new CronException("A crontab field value cannot be empty."));

            //
            // Next, look for a list of values (e.g. 1,2,3).
            //

            if (str.IndexOf(',') > 0)
            {
                var result = success;
                using var token = ((IEnumerable<string>)str.Split(StringSeparatorStock.Comma)).GetEnumerator();
                while (token.MoveNext() && result == null)
                    result = InternalParse(token.Current, acc, success, errorSelector);
                return result;
            }

            int? every = null;

            //
            // Look for stepping first (e.g. */2 = every 2nd).
            //

            if (str.IndexOf('/') is var slashIndex and > 0)
            {
                every = int.Parse(str.Substring(slashIndex + 1), CultureInfo.InvariantCulture);
                str = str.Substring(0, slashIndex);
            }

            //
            // Next, look for wildcard (*).
            //

            if (str.Length == 1 && str[0] == '*')
            {
                return acc(-1, -1, every ?? 1, success, errorSelector);
            }

            //
            // Next, look for a range of values (e.g. 2-10).
            //

            if (str.IndexOf('-') is var dashIndex and > 0)
            {
                var first = ParseValue(str.Substring(0, dashIndex));
                var last = ParseValue(str.Substring(dashIndex + 1));

                return acc(first, last, every ?? 1, success, errorSelector);
            }

            //
            // Finally, handle the case where there is only one number.
            //

            var value = ParseValue(str);

            return every is { } someEvery
                 ? acc(value, MaxValue, someEvery, success, errorSelector)
                 : acc(value, value, 1, success, errorSelector);

            int ParseValue(string str)
            {
                if (str.Length == 0)
                    throw new CronException("A crontab field value cannot be empty.");

                if (str[0] is >= '0' and <= '9')
                    return int.Parse(str, CultureInfo.InvariantCulture);

                if (this.names == null)
                {
                    throw new CronException($"'{str}' is not a valid [{Kind}] crontab field value. It must be a numeric value between {MinValue} and {MaxValue} (all inclusive).");
                }

                for (var i = 0; i < this.names.Length; i++)
                {
                    if (Comparer.IsPrefix(this.names[i], str, CompareOptions.IgnoreCase))
                        return i + MinValue;
                }

                var names = string.Join(", ", this.names);
                throw new CronException($"'{str}' is not a known value name. Use one of the following: {names}.");
            }
        }
    }
}
